Enable smart HTTP under /p/ URLs The new smart HTTP Git protocol is now supported through the Gerrit web application context by using the URL /p/$project.git. Like SSH, push requires user authentication. However, since user authentication is currently based solely upon HTTP cookies, and cookies aren't supported by the C Git command line client, using push is not currently possible. We need to introduce our own password based user authentication process. Change-Id: I76a894226a583ce6f47fd246f7d0f88b77ea4505 Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml index fa9a919..d358597 100644 --- a/gerrit-httpd/pom.xml +++ b/gerrit-httpd/pom.xml
@@ -38,6 +38,11 @@ <artifactId>servlet-api</artifactId> <scope>provided</scope> </dependency> + + <dependency> + <groupId>org.eclipse.jgit</groupId> + <artifactId>org.eclipse.jgit.http.server</artifactId> + </dependency> <dependency> <groupId>org.eclipse.jgit</groupId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java new file mode 100644 index 0000000..49ef78b --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
@@ -0,0 +1,210 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd; + +import com.google.gerrit.common.PageLinks; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.Nullable; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.ReceiveCommits; +import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectControl; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.http.server.GitServlet; +import org.eclipse.jgit.http.server.resolver.AsIsFileService; +import org.eclipse.jgit.http.server.resolver.ReceivePackFactory; +import org.eclipse.jgit.http.server.resolver.RepositoryResolver; +import org.eclipse.jgit.http.server.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.http.server.resolver.UploadPackFactory; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.UploadPack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Serves Git repositories over HTTP. */ +@Singleton +public class ProjectServlet extends GitServlet { + private static final Logger log = + LoggerFactory.getLogger(ProjectServlet.class); + + private static final String ATT_CONTROL = ProjectControl.class.getName(); + + static class Module extends AbstractModule { + @Override + protected void configure() { + bind(Resolver.class); + bind(Upload.class); + bind(Receive.class); + } + } + + static ProjectControl getProjectControl(HttpServletRequest req) + throws ServiceNotEnabledException { + ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL); + if (pc == null) { + log.error("No " + ATT_CONTROL + " in request", new Exception("here")); + throw new ServiceNotEnabledException(); + } + return pc; + } + + private final Provider<String> urlProvider; + + @Inject + ProjectServlet(final Resolver resolver, final Upload upload, + final Receive receive, + @CanonicalWebUrl @Nullable Provider<String> urlProvider) { + this.urlProvider = urlProvider; + + setRepositoryResolver(resolver); + setAsIsFileService(AsIsFileService.DISABLED); + setUploadPackFactory(upload); + setReceivePackFactory(receive); + } + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + + serveRegex("^/(.*?)/?$").with(new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) + throws IOException { + ProjectControl pc; + try { + pc = getProjectControl(req); + } catch (ServiceNotEnabledException e) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + Project.NameKey dst = pc.getProject().getNameKey(); + StringBuilder r = new StringBuilder(); + r.append(urlProvider.get()); + r.append('#'); + r.append(PageLinks.toProject(dst, Change.Status.NEW)); + rsp.sendRedirect(r.toString()); + } + }); + } + + static class Resolver implements RepositoryResolver { + private final GitRepositoryManager manager; + private final ProjectControl.Factory projectControlFactory; + + @Inject + Resolver(GitRepositoryManager manager, + ProjectControl.Factory projectControlFactory) { + this.manager = manager; + this.projectControlFactory = projectControlFactory; + } + + @Override + public Repository open(HttpServletRequest req, String projectName) + throws RepositoryNotFoundException { + if (projectName.endsWith(".git")) { + // Be nice and drop the trailing ".git" suffix, which we never keep + // in our database, but clients might mistakenly provide anyway. + // + projectName = projectName.substring(0, projectName.length() - 4); + } + + if (projectName.startsWith("/")) { + // Be nice and drop the leading "/" if supplied by an absolute path. + // We don't have a file system hierarchy, just a flat namespace in + // the database's Project entities. We never encode these with a + // leading '/' but users might accidentally include them in Git URLs. + // + projectName = projectName.substring(1); + } + + final ProjectControl pc; + try { + final Project.NameKey nameKey = new Project.NameKey(projectName); + pc = projectControlFactory.validateFor(nameKey); + } catch (NoSuchProjectException err) { + throw new RepositoryNotFoundException(projectName); + } + req.setAttribute(ATT_CONTROL, pc); + + return manager.openRepository(pc.getProject().getName()); + } + } + + static class Upload implements UploadPackFactory { + @Override + public UploadPack create(HttpServletRequest req, Repository db) + throws ServiceNotEnabledException { + // The Resolver above already checked READ access for us. + // + ProjectControl pc = getProjectControl(req); + UploadPack up = new UploadPack(db); + return up; + } + } + + static class Receive implements ReceivePackFactory { + private final ReceiveCommits.Factory factory; + + @Inject + Receive(final ReceiveCommits.Factory factory) { + this.factory = factory; + } + + @Override + public ReceivePack create(HttpServletRequest req, Repository db) + throws ServiceNotEnabledException, ServiceNotAuthorizedException { + final ProjectControl pc = getProjectControl(req); + if (pc.getCurrentUser() instanceof IdentifiedUser) { + final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser(); + final ReceiveCommits rc = factory.create(pc, db); + final ReceiveCommits.Capable s = rc.canUpload(); + if (s != ReceiveCommits.Capable.OK) { + // TODO We should alert the user to this message on the HTTP + // response channel, assuming Git will even report it to them. + // + final String who = user.getUserName(); + final String why = s.getMessage(); + log.warn("Rejected push from " + who + ": " + why); + throw new ServiceNotEnabledException(); + } + + rc.getReceivePack().setRefLogIdent(user.newRefLogIdent()); + return rc.getReceivePack(); + + } else { + throw new ServiceNotAuthorizedException(); + } + } + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java index 30e9972..b2cf3f0 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -49,6 +49,7 @@ serve("/signout").with(HttpLogoutServlet.class); serve("/ssh_info").with(SshInfoServlet.class); serve("/static/*").with(StaticServlet.class); + serve("/p/*").with(ProjectServlet.class); serve("/Main.class").with(notFound()); serve("/com/google/gerrit/launcher/*").with(notFound()); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java index 0c34398..8df15f9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -118,6 +118,7 @@ install(new UrlModule()); install(new UiRpcModule()); install(new GerritRequestModule()); + install(new ProjectServlet.Module()); bind(SshInfo.class).toProvider(sshInfoProvider); bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
diff --git a/pom.xml b/pom.xml index 2d07f6a..13e4b9c 100644 --- a/pom.xml +++ b/pom.xml
@@ -633,6 +633,12 @@ </dependency> <dependency> + <groupId>org.eclipse.jgit</groupId> + <artifactId>org.eclipse.jgit.http.server</artifactId> + <version>${jgitVersion}</version> + </dependency> + + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.2</version>